/**
 * Copyright 2011 Adrian Witas
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.abstractmeta.code.g.core.code.builder;


import com.google.common.base.Preconditions;
import org.abstractmeta.code.g.code.*;
import org.abstractmeta.code.g.code.handler.ConstructorHandler;
import org.abstractmeta.code.g.code.handler.FieldHandler;
import org.abstractmeta.code.g.code.handler.MethodHandler;
import org.abstractmeta.code.g.code.handler.TypeHandler;
import org.abstractmeta.code.g.core.code.JavaTypeImpl;
import org.abstractmeta.code.g.core.code.JavaTypeImporterImpl;
import org.abstractmeta.code.g.core.generator.ContextImpl;
import org.abstractmeta.code.g.core.util.JavaTypeUtil;
import org.abstractmeta.code.g.core.util.ReflectUtil;
import org.abstractmeta.code.g.generator.Context;

import java.lang.annotation.Annotation;
import java.lang.reflect.GenericArrayType;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.util.*;


/**
 * Provide generateBuilder implementation of org.abstractmeta.toolbox.code.JavaType
 * This class has been auto-generated by code-g.
 */
public class JavaTypeBuilderImpl implements JavaTypeBuilder {

    public static final String CODE_G_GENERATOR_SIGNATURE = "This source code was automatically generated by code-g generator.";

    private final JavaType sourceType;

    private final JavaKind kind;

    private final String name;

    private final String simpleName;

    private final String packageName;


    private List<FieldHandler> fieldHandlers = new LinkedList<FieldHandler>();

    private List<TypeHandler> typeHandlers = new LinkedList<TypeHandler>();

    private final List<MethodHandler> methodHandlers = new LinkedList<MethodHandler>();

    private final List<ConstructorHandler> constructorHandlers = new LinkedList<ConstructorHandler>();

    private List<JavaField> fields = new LinkedList<JavaField>();

    private List<JavaMethod> methods = new LinkedList<JavaMethod>();

    private List<JavaConstructor> constructors = new LinkedList<JavaConstructor>();

    private List<Type> genericTypeArguments = new LinkedList<Type>();

    private Map<String, Type> genericTypeVariables = new HashMap<String, Type>();

    private Set<Type> importTypes = new HashSet<Type>();

    private List<Type> superInterfaces = new LinkedList<Type>();


    private List<String> bodyLines = new LinkedList<String>();

    private Type superType;

    private List<JavaType> nestedJavaTypes = new LinkedList<JavaType>();

    private List<JavaModifier> modifiers = new LinkedList<JavaModifier>();


    private List<Annotation> annotations = new LinkedList<Annotation>();

    private List<String> documentation = new LinkedList<String>();

    private boolean nested;

    private JavaTypeImporter javaTypeImporter;

    private final Context context;

    private final Collection<String> classPathDependencies = new LinkedList<String>();

    public JavaTypeBuilderImpl(String typeName) {
        this(typeName, new ContextImpl());
    }

    public JavaTypeBuilderImpl(String typeName, Context context) {
        this(JavaKind.CLASS, typeName, context);
    }

    public JavaTypeBuilderImpl(JavaKind javaKind, String typeName) {
        this(javaKind, typeName, new ContextImpl());
    }

    public JavaTypeBuilderImpl(JavaKind javaKind, String typeName, Context context) {
        this(javaKind, typeName, null, context);
    }

    public JavaTypeBuilderImpl(JavaKind javaKind, String typeName, JavaType sourceType, Context context) {
        Preconditions.checkNotNull(javaKind, "javaKind was null");
        Preconditions.checkNotNull(typeName, "typeName was null");
        Preconditions.checkNotNull(context, "context was null");
        this.sourceType = sourceType;
        this.context = context;
        this.packageName = extractPackageName(typeName);
        this.kind = javaKind;
        this.name = typeName;
        this.simpleName = getSimpleName(typeName);
        this.javaTypeImporter = new JavaTypeImporterImpl(packageName);
    }

    @Override
    public Collection<String> getClassPathDependencies() {
        return classPathDependencies;
    }

    @Override
    public JavaTypeBuilder addClassPathDependency(String... classPathDependencies) {
        Collections.addAll(this.classPathDependencies, classPathDependencies);
        return this;
    }

    @Override
    public JavaTypeBuilder addClassPathDependency(Collection<String> classPathDependencies) {
        this.classPathDependencies.addAll(classPathDependencies);
        return this;
    }

    @Override
    public JavaType getSourceType() {
        return sourceType;
    }


    @Override
    public JavaTypeImporter getImporter() {
        return javaTypeImporter;
    }

    public String extractPackageName(String typeName) {
        int dotIndex = typeName.lastIndexOf('.');
        if (dotIndex != -1) {
            return typeName.substring(0, dotIndex);

        } else {
            return "";
        }
    }


    @Override
    public JavaTypeBuilder addFieldHandlers(FieldHandler... fieldHandlers) {
        Collections.addAll(this.fieldHandlers, fieldHandlers);
        return this;
    }

    @Override
    public JavaTypeBuilder addTypeHandlers(TypeHandler... typeHandlers) {
        Collections.addAll(this.typeHandlers, typeHandlers);
        return this;
    }

    @Override
    public JavaTypeBuilder addMethodHandlers(MethodHandler... methodHandlers) {
        Collections.addAll(this.methodHandlers, methodHandlers);
        return this;
    }

    @Override
    public JavaTypeBuilder addConstructorHandlers(ConstructorHandler... constructorHandlers) {
        Collections.addAll(this.constructorHandlers, constructorHandlers);
        return this;
    }

    @Override
    public JavaTypeBuilder addFields(JavaField... fields) {
        for (JavaField field : fields) {
            addField(field);
        }
        return this;
    }

    @Override
    public JavaTypeBuilder addFields(Collection<JavaField> fields) {
        for (JavaField field : fields) {
            addField(field);
        }
        return this;

    }

    @Override
    public JavaTypeBuilder setFields(List<JavaField> fields) {
        Preconditions.checkNotNull(fields, "fields was null");
        this.fields = fields;
        return this;
    }

    public List<JavaField> getFields() {
        return this.fields;
    }


    @Override
    public boolean containsField(String fieldName) {
        for (JavaField field : getFields()) {
            if (fieldName.equals(field.getName())) {
                return true;
            }
        }
        return false;
    }

    @Override
    public JavaField getFiled(String fieldName) {
        for (JavaField field : getFields()) {
            if (fieldName.equals(field.getName())) {
                return field;
            }
        }
        return null;
    }

    @Override
    public JavaTypeBuilder addField(JavaField field) {
        for (FieldHandler handler : fieldHandlers) {
            context.replace(JavaField.class, field);
            handler.handle(this, field, context);
            context.remove(JavaField.class);
        }
        fields.add(field);
        return this;
    }

    @Override
    public boolean containsMethod(String methodName, JavaParameter... matchingParameters) {
        return getMethod(methodName, matchingParameters) != null;
    }

    @Override
    public JavaMethod getMethod(String methodName, JavaParameter... matchingParameters) {
        List<Class> argumentTypes = JavaTypeUtil.getParameterClasses(Arrays.asList(matchingParameters));
        return JavaTypeUtil.getMethod(getMethods(), methodName, argumentTypes.toArray(new Class[]{}));

//        outer_loop:
//        for (JavaMethod methodCandidate : getMethods()) {
//            if (methodName.equals(methodCandidate.getName())) {
//                if (matchingParameters == null || matchingParameters.length == 0) {
//                    return methodCandidate;
//                }
//                if (matchingParameters.length != methodCandidate.getParameters().size()) continue;
//                for (int i = 0, matchingParametersLength = matchingParameters.length; i < matchingParametersLength; i++) {
//                    JavaParameter matchingParameter = matchingParameters[i];
//                    JavaParameter candidateParameter = methodCandidate.getParameters().get(i);
//                    if (!matchingParameter.getType().equals(candidateParameter.getType())) {
//                        continue outer_loop;
//                    }
//                }
//                return methodCandidate;
//            }
//        }
//        return null;
    }

    @Override
    public JavaTypeBuilder addMethod(JavaMethod method) {
        for (MethodHandler methodHandler : methodHandlers) {
            context.replace(JavaMethod.class, method);
            methodHandler.handle(this, method, context);
            context.remove(JavaMethod.class);
        }
        this.methods.add(method);
        return this;
    }


    @Override
    public List<JavaMethod> getMethods(String methodName) {
        List<JavaMethod> result = new ArrayList<JavaMethod>();
        for (JavaMethod method : getMethods()) {
            if (methodName.equals(method.getName())) {
                result.add(method);
            }
        }
        return result;
    }


    public List<JavaMethod> getMethods() {
        return this.methods;
    }

    @Override
    public JavaTypeBuilder addMethods(JavaMethod... methods) {
        for (JavaMethod method : methods) {
            addMethod(method);
        }
        return this;
    }

    @Override
    public JavaTypeBuilder addMethods(Collection<JavaMethod> methods) {
        this.methods.addAll(methods);
        return this;
    }

    @Override
    public JavaTypeBuilder setMethods(List<JavaMethod> methods) {
        Preconditions.checkNotNull(methods, "methods was null");
        this.methods = methods;
        return this;
    }

    @Override
    public JavaTypeBuilder addConstructor(JavaConstructor constructor) {
        for (ConstructorHandler handler : constructorHandlers) {
            context.replace(JavaConstructor.class, constructor);
            handler.handle(this, constructor, context);
            context.remove(JavaConstructor.class);
        }
        constructors.add(constructor);
        return this;
    }

    @Override
    public JavaTypeBuilder addConstructors(JavaConstructor... constructors) {
        for (JavaConstructor constructor : constructors) {
            addConstructor(constructor);
        }
        return this;
    }

    @Override
    public JavaTypeBuilder addConstructors(Collection<JavaConstructor> constructors) {
        for (JavaConstructor constructor : constructors) {
            addConstructor(constructor);
        }
        return this;
    }

    @Override
    public JavaTypeBuilder setConstructors(List<JavaConstructor> constructors) {
        Preconditions.checkNotNull(constructors, "constructors was null");
        this.constructors = constructors;
        return this;
    }

    public List<JavaConstructor> getConstructors() {
        return this.constructors;
    }

    @Override
    public JavaTypeBuilder addSuperInterfaces(Type... superInterfaces) {
        Collection<Type> collection = new ArrayList<Type>();
        Collections.addAll(collection, superInterfaces);
        addSuperInterfaces(collection);
        return this;
    }

    public List<Type> getSuperInterfaces() {
        return this.superInterfaces;
    }


    @Override
    public JavaTypeBuilder addSuperInterfaces(Collection<Type> superInterfaces) {
        this.superInterfaces.addAll(superInterfaces);
        return this;
    }


    @Override
    public JavaTypeBuilder addImportTypes(Type... importTypes) {
        Collections.addAll(this.importTypes, importTypes);
        return this;
    }


    public Set<Type> getImportTypes() {
        return this.importTypes;
    }

    public JavaTypeBuilder addImportType(Type... importTypes) {
        Collections.addAll(this.importTypes, importTypes);
        return this;
    }

    @Override
    public JavaTypeBuilder addImportTypes(Collection<Type> importTypes) {
        this.importTypes.addAll(importTypes);
        return this;
    }

    public String getPackageName() {
        return this.packageName;
    }

    public JavaKind getKind() {
        return this.kind;
    }

    @Override
    public JavaTypeBuilder addBodyLines(String... bodyLines) {
        Collections.addAll(this.bodyLines, bodyLines);
        return this;
    }

    @Override
    public JavaTypeBuilder addBodyLines(Collection<String> bodyLines) {
        return this;
    }

    public List<String> getBodyLines() {
        return this.bodyLines;
    }

    public Type getSuperType() {
        return this.superType;
    }

    @Override
    public JavaTypeBuilder setSuperType(Type superType) {
        this.superType = superType;
        return this;
    }

    public List<JavaType> getNestedJavaTypes() {
        return this.nestedJavaTypes;
    }


    @Override
    public JavaTypeBuilder addNestedJavaTypes(JavaType... classTypes) {
        Collections.addAll(this.nestedJavaTypes, classTypes);
        return this;
    }

    @Override
    public JavaTypeBuilder addNestedJavaTypes(Collection<JavaType> classTypes) {
        this.nestedJavaTypes.addAll(classTypes);
        return this;
    }

    @Override
    public JavaTypeBuilder addModifiers(Collection<JavaModifier> modifiers) {
        this.modifiers.addAll(modifiers);
        return this;
    }


    @Override
    public JavaTypeBuilder addModifiers(JavaModifier... modifiers) {
        Collections.addAll(this.modifiers, modifiers);
        return this;
    }

    public List<JavaModifier> getModifiers() {
        return this.modifiers;
    }

    public String getName() {
        return this.name;
    }

    @Override
    public String getSimpleName() {
        return simpleName;
    }

    public String getSimpleName(String name) {
        if (name == null) {
            throw new IllegalArgumentException("name was null");
        }
        name = name.replace('$', '.');
        int dotIndex = name.lastIndexOf('.');
        if (dotIndex != -1) {
            return name.substring(dotIndex + 1, name.length());
        } else {
            return name;
        }
    }


    @Override
    public JavaTypeBuilder addAnnotations(Annotation... annotations) {
        Collections.addAll(this.annotations, annotations);
        return this;
    }

    public List<Annotation> getAnnotations() {
        return this.annotations;
    }

    @Override
    public JavaTypeBuilder addAnnotations(Collection<Annotation> annotations) {
        this.annotations.addAll(annotations);
        return this;
    }

    @Override
    public JavaTypeBuilder addDocumentations(String... documentations) {
        return this;
    }

    public List<String> getDocumentation() {
        return this.documentation;
    }


    @Override
    public JavaTypeBuilder addDocumentation(Collection<String> documentation) {
        this.documentation.addAll(documentation);
        return this;
    }

    @Override
    public JavaTypeBuilder setNested(boolean nested) {
        this.nested = nested;
        return this;
    }

    public boolean isNested() {
        return nested;
    }

    @Override
    public JavaTypeBuilder addGenericTypeArguments(Type... genericTypeArguments) {
        Collections.addAll(this.genericTypeArguments, genericTypeArguments);
        return this;
    }

    @Override
    public JavaTypeBuilder addGenericTypeArguments(Collection<Type> genericTypeArguments) {
        this.genericTypeArguments.addAll(genericTypeArguments);
        return this;
    }

    public Map<String, Type> getGenericTypeVariables() {
        return genericTypeVariables;
    }

    @Override
    public List<Type> getGenericTypeArguments() {
        return genericTypeArguments;
    }

    @Override
    public JavaTypeBuilder addGenericTypeVariables(Map<String, Type> genericTypeVariables) {
        this.genericTypeVariables.putAll(genericTypeVariables);
        return this;
    }

    @Override
    public JavaTypeBuilder addGenericTypeVariable(String key, Type value) {
        this.genericTypeVariables.put(key, value);
        this.javaTypeImporter.getGenericTypeVariables().put(key, value);
        return this;
    }

    public Context getContext() {
        return context;
    }

    @Override
    public JavaType build() {
        for (TypeHandler typeHandler : typeHandlers) {
            typeHandler.handle(this, context);
        }

        updatedImportedTypes(javaTypeImporter);
        for (JavaType nestedType : nestedJavaTypes) {
            importTypes.addAll(nestedType.getImportTypes());

        }
        resolveGenerics();
        return new JavaTypeImpl(fields, methods, constructors, importTypes, superInterfaces, packageName, kind, bodyLines, superType, nestedJavaTypes, modifiers, name, annotations, documentation, nested, simpleName, genericTypeArguments, genericTypeVariables, classPathDependencies);
    }


    protected void updatedImportedTypes(JavaTypeImporter importer) {
        for (JavaMethod method : methods) {
            importer.addTypes(method.getResultType());
            importer.addTypes(method.getExceptionTypes());
            for(Annotation annotation: method.getAnnotations()) {
                importer.addTypes(annotation.annotationType());
            }
            for (JavaParameter parameter : method.getParameters()) {
                importer.addTypes(parameter.getType());
            }
        }
        for(Type iface: this.superInterfaces) {
            importer.addTypes(iface);
        }
        if(superType != null) {
            importer.addTypes(superType);
        }
        for (JavaField field : fields) {
            importer.addTypes(field.getType());
        }
        for (String typeName : importer.getImportTypeNames()) {
            Type importedType = importer.getType(typeName);
            Class importedClass = ReflectUtil.getRawClass(importedType);
            if (importedClass != null) {
                if (!this.importTypes.contains(importedClass)) {
                    this.importTypes.add(importedClass);
                }
            }
        }
    }

    protected void resolveGenerics() {
        resolveInterfaceTypeVariables();
        resolveFieldsTypeVariables();
        resolveGenericTypeArguments();
        Map<String, TypeVariable> typeVariables = getTypeVariables();
        for (Type typeArgument : genericTypeArguments) {
            if (typeArgument instanceof TypeVariable) {
                typeVariables.remove(TypeVariable.class.cast(typeArgument).getName());
            }
        }
        genericTypeArguments.addAll(typeVariables.values());

    }

    protected void resolveGenericTypeArguments() {
        List<Type> genericTypeArguments = new ArrayList<Type>(this.genericTypeArguments);
        this.genericTypeArguments.clear();
        for (Type type : genericTypeArguments) {
            Type resolvedType = ReflectUtil.resolveTypeVariables(type, genericTypeVariables);
            if (resolvedType instanceof Class) continue;
            this.genericTypeArguments.add(resolvedType);

        }

    }

    protected void resolveInterfaceTypeVariables() {
        Collection<Type> ifaces = new ArrayList<Type>(this.superInterfaces);
        superInterfaces.clear();
        for (Type iface : ifaces) {
            this.superInterfaces.add(ReflectUtil.resolveTypeVariables(iface, genericTypeVariables));
        }
    }

    protected void resolveFieldsTypeVariables() {
        Collection<JavaField> fields = new ArrayList<JavaField>(this.fields);
        this.fields.clear();
        for (JavaField field : fields) {
            this.fields.add(new JavaFieldBuilder().merge(field).setType(ReflectUtil.resolveTypeVariables(field.getType(), genericTypeVariables)).build());
        }
    }


    protected Map<String, TypeVariable> getTypeVariables() {
        List<TypeVariable> variables = new ArrayList<TypeVariable>();
        for (Type superInterface : superInterfaces) {
            addTypeVariables(superInterface, variables);
        }
        if (superType != null) addTypeVariables(superType, variables);
        for (JavaField field : fields) {
            addTypeVariables(field.getType(), variables);
        }
        Map<String, TypeVariable> result = new HashMap<String, TypeVariable>();
        for (TypeVariable variable : variables) {
            result.put(variable.getName(), variable);
        }
        return result;
    }


    protected void addTypeVariables(Type sourceType, List<TypeVariable> result) {
        if (sourceType instanceof ParameterizedType) {
            ParameterizedType parameterizedType = ParameterizedType.class.cast(sourceType);
            for (Type argumentType : parameterizedType.getActualTypeArguments()) {
                addTypeVariables(argumentType, result);
            }
        } else if (sourceType instanceof TypeVariable) {
            result.add(TypeVariable.class.cast(sourceType));
        } else if (sourceType instanceof GenericArrayType) {
            GenericArrayType arrayType = GenericArrayType.class.cast(sourceType);
            addTypeVariables(arrayType.getGenericComponentType(), result);
        }
    }

    @Override
    public JavaTypeBuilder merge(JavaType instance) {


        if (instance.getFields() != null) {
            addFields(instance.getFields());
        }
        if (instance.getMethods() != null) {
            addMethods(instance.getMethods());
        }
        if (instance.getConstructors() != null) {
            addConstructors(instance.getConstructors());
        }
        if (instance.getImportTypes() != null) {
            addImportTypes(instance.getImportTypes());
        }
        if (instance.getSuperInterfaces() != null) {
            addSuperInterfaces(instance.getSuperInterfaces());
        }
        if (instance.getBodyLines() != null) {
            addBodyLines(instance.getBodyLines());
        }
        if (instance.getSuperType() != null) {
            setSuperType(instance.getSuperType());
        }
        if (instance.getNestedJavaTypes() != null) {
            addNestedJavaTypes(instance.getNestedJavaTypes());
        }
        if (instance.getModifiers() != null) {
            addModifiers(instance.getModifiers());
        }
        if (instance.getAnnotations() != null) {
            addAnnotations(instance.getAnnotations());
        }
        if (instance.getDocumentation() != null) {
            addDocumentation(instance.getDocumentation());
        }
        if (instance.getGenericTypeArguments() != null) {
            addGenericTypeArguments(instance.getGenericTypeArguments());
        }
        if (instance.getGenericTypeVariables() != null) {
            addGenericTypeVariables(instance.getGenericTypeVariables());
        }

        if (instance.isNested()) {
            nested = instance.isNested();
        }
        return this;
    }

}